// (C) 2021 Intel Corporation. All rights reserved.
// Your use of Intel Corporation's design tools, logic functions and other 
// software and tools, and its AMPP partner logic functions, and any output 
// files from any of the foregoing (including device programming or simulation 
// files), and any associated documentation or information are expressly subject 
// to the terms and conditions of the Intel Program License Subscription 
// Agreement, Intel FPGA IP License Agreement, or other applicable 
// license agreement, including, without limitation, that your use is for the 
// sole purpose of programming logic devices manufactured by Intel and sold by 
// Intel or its authorized distributors.  Please refer to the applicable 
// agreement for further details.

module pca9555 #(parameter DEVICE_ADDR    = 7'h55,       //device address Master needs to use to access this device
                 parameter P_RV_OUTPUT0   = 8'hFF,       //Reset Value for OUTPUT0 register (addr 0x2)
                 parameter P_RV_POLARITY0 = 8'h00,       //Reset Value for POLARITY0 register (addr 0x4)
                 parameter P_RV_CONFIG0   = 8'hFF,       //Reset Value for CONFIG0 register (addr 0x6)
                 parameter P_RV_OUTPUT1   = 8'hFF,       //Reset Value for OUTPUT1 register (addr 0x3)
                 parameter P_RV_POLARITY1 = 8'h00,       //Reset Value for POLARITY1 register (addr 0x5)
                 parameter P_RV_CONFIG1   = 8'hFF        //Reset Value for CONFIG1 register (addr 0x7)
                 )
   (
    input       iClk,   //input clock for WDT (2MHz)
    input       iRst_n, //input reset signal (active low)
    input       iSCL, //input clock signal for I2C/SMBUS
    inout       ioSDA, //input data signal from

    inout [7:0] ioDataP0,
    inout [7:0] ioDataP1,

    output      oSmbAlert_n   
    );

   ////////////////////////////////////
   //local param declarations
   ///////////////////////////////////
   
   //FSM STATES      
   localparam INIT = 0;
   localparam DEV_ADDR = 1;
   localparam ACK_ST = 2;
   localparam REG_ADDR = 3;
   localparam REG_WRITE = 4;
   localparam REG_READ = 5;
   localparam MASTER_ACK = 6;


   //register addresses
   localparam INPUT0 = 3'h0;
   localparam INPUT1 = 3'h1;
   localparam OUTPUT0 = 3'h2;
   localparam OUTPUT1 = 3'h3;
   localparam POLARITY0 = 3'h4;
   localparam POLARITY1 = 3'h5;
   localparam CONFIG0 = 3'h6;
   localparam CONFIG1 = 3'h7;

   /////////////////////////////////////////////////////////////////////////////////////////////////
   //Internal register declarations
   ///////////////////////////////////


   reg   rSDAo;                   //used to put the read data from regs (or the slave ACK) data
   
   
   reg   start_fsm;               //used to signal when a START condition has been detected
   reg   stop_fsm;                //used to signal when a STOP condition has been detected

   reg [2:0] rI2CFsm;             //I2C FSM, clocked by SCL input signal from link
   reg [2:0] fsm_prev_st;         //stores from where you are jumping into ACK_ST state
   

   reg [7:0] rDevAddr;            //Used to store the recieved Device Address from I2C/SMBus link. This needs
                                  //to be compared with current device address param to execute further or not
   
   reg [2:0] cnt;                 //used to parallelize/serialize data

   reg [7:0] rRegAddr;           //to store internal register address to be addressed

   reg       ack_ena;             //asserted when ACK should be sent to Master
   reg       read_ena;            //asserted during register read cycle. Together with reg_address will select the output data to be transmitted thru SDA inout

   reg [7:0] rInput0;             //these are the 8 registers (8bits) the PCA9555 has, from ADDR = 0 to 7
   reg [7:0] rInput1;

   reg [7:0] rInput0_meta = 8'h0;
   reg [7:0] rInput1_meta = 8'h0;

   reg [7:0] rInput0_sync = 8'h0;
   reg [7:0] rInput1_sync = 8'h0;

   reg [7:0] rInput0_t;
   reg [7:0] rInput1_t;
   
   reg [7:0] rOutput0;            //write/read register thru I2C/SMBus I/F. to Port 0 output pins
   reg [7:0] rOutput1;            //write/read register thru I2C/SMBus I/F. to Port 1 output pins
   reg [7:0] rPolarity0;          //write/read register thru I2C/SMBus I/F. Configures polarity on how Port 0 input register should be read
   reg [7:0] rPolarity1;          //write/read register thru I2C/SMBus I/F. Configures polarity on how Port 1 input register should be read
   reg [7:0] rConfig0;            //write/read register thru I2C/SMBus I/F. Configures Port 0 IO bits as inputs (HIGH) or outputs (LOW)
   reg [7:0] rConfig1;            //write/read register thru I2C/SMBus I/F. Configures Port 1 IO bits as inputs (HIGH) or outputs (LOW)

   reg       proxy_bit;
   reg       filter_flag;

   reg       read_flag;       //it is used to distinguish if it is the 1st byte or 2nd byte of the read command (to know where to end the read)
   
   reg       ack_out;         //indicates acknowledge is asserted and the inout data pin (ioSDA) is as output 
   reg       read_out;        //indicates data is being read and the inout data pin (ioSDA) is as output 
   

   reg       start_clr_n;     //after start condition is detected, as it's latched, needs to be cleared
   wire      start_rst_n;     //these 2 signals are for this purpose

   wire      wSmbAlert0_n;    //used to distinguish if any input changed its value and alert needs to be asserted
   wire      wSmbAlert1_n;

   wire      wDoneTimer1ms;
   
   ///////////////////////////////////////
   //internal wire declarations
   //////////////////////////////////////

   reg [2:0] scl_t;
   reg [2:0] sda_t;
   reg       scl_rise;
   reg       scl_fall;


   //////////////////////////

   always @(posedge iClk, negedge iRst_n)
     begin
        if (~iRst_n)
          begin
             scl_t <= 3'b111;
             scl_rise <= 1'b0;
             scl_fall <= 1'b0;

             sda_t <= 3'b111;
             
          end
        else
          begin
             scl_t[0] <= iSCL;
             scl_t[1] <= scl_t[0];
             scl_t[2] <= scl_t[1];
             scl_rise <= !scl_t[2] && scl_t[1];

             scl_fall <= scl_t[2] && !scl_t[1];

             ///

             sda_t[0] <= ioSDA;
             sda_t[1] <= sda_t[0];
             if (sda_t[1] == sda_t[0]) 
               sda_t[2] <= sda_t[1];
             
          end
     end

   //1-msec Timer, started when read_out or ack_out are HIGH, clear when both low
   delay #(.COUNT(2000)) 
     Timer1ms(
               .iClk(iClk),
               .iRst(iRst_n),
               .iStart(read_out || ack_out),
               .iClrCnt(1'b0),
               .oDone(wDoneTimer1ms)
               );
               
   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

   //registering the ioData Ports 0&1 into the respective input register port
   always @(negedge iSCL, negedge iRst_n)
     begin
        if (!iRst_n)
          begin
             rInput0 <= 8'h00;
             rInput1 <= 8'h00;
          end
        else
          begin
             rInput0 <= ioDataP0;
             rInput1 <= ioDataP1;
          end
     end

   assign start_rst_n = !iRst_n || !start_clr_n;
   
   //This is to detect the start condition in the I2C/SMBUS link
   always @(negedge sda_t[2], posedge start_rst_n)
     begin
        if (start_rst_n)
          begin
             start_fsm <= 1'b0;
          end
        else
          begin
             if (iSCL)
               begin
                  start_fsm <= 1'b1;                     //asserts when I2C start condition is detected: a change on data signal (from HIGH to LOW) while clk is HIGH
               end
          end // else: !if(~iRst_n)
     end // always @ (negedge ioSDA, negedge iRst_n)

   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   
   //This is to detect the stop condition in the I2C/SMBUS link
   always @(posedge sda_t[2], posedge start_fsm)
     begin
        if (start_fsm)
          begin
             stop_fsm <= 1'b0;
          end
        else
          begin
             if (iSCL)
               begin
                  stop_fsm <= 1'b1;                     //asserts when I2C start condition is detected: a change on data signal (from HIGH to LOW) while clk is HIGH
               end
             else
               begin
                  stop_fsm <= 1'b0;                     //deasseted the rest of the times
               end
          end // else: !if(~iRst_n)
     end // always @ (negedge ioSDA, negedge iRst_n)

   
   always @(posedge iClk, negedge iRst_n)
     begin
        if (~iRst_n)
          begin
             rI2CFsm <= INIT;                           //INIT state waits for START condition to go forward
             fsm_prev_st <= INIT;
             rDevAddr <= 8'h00;
             rRegAddr <= 8'h00;
             cnt <= 3'h7;
             ack_ena <= 1'b0;
             read_ena <= 1'b0;
             rOutput0 <= P_RV_OUTPUT0;
             rOutput1 <= P_RV_OUTPUT1;
             rPolarity0 <= P_RV_POLARITY0;
             rPolarity1 <= P_RV_POLARITY1;
             rConfig0 <= P_RV_CONFIG0;
             rConfig1 <= P_RV_CONFIG1;
             start_clr_n <= 1'b1;
             proxy_bit <= 1'b0;
             filter_flag <= 1'b0;
             read_flag <= 1'b1;
          end
        else if (scl_rise)
          begin
             //if at any moment a STOP or a START are received, FSM needs to act properly, so these conditions are placed here to be checked at every cycle (every state)
             if (stop_fsm == 1'b1)        //stop indication received
               begin
                  rI2CFsm <= INIT;    
                  cnt <= 3'h7;
                  fsm_prev_st <= INIT;
                  rDevAddr <= 8'h00;
                  rRegAddr <= 8'h00;
                  cnt <= 3'h7;
                  ack_ena <= 1'b0;
                  read_ena <= 1'b0;
                  //start_clr_n <= 1'b1;
                  proxy_bit <= 1'b0;

                  
               end
             else if (start_fsm == 1'b1 && rI2CFsm != INIT)  //restart received
               begin
                  rDevAddr[7] <= sda_t[2];
                  rI2CFsm <= DEV_ADDR;
                  fsm_prev_st <= DEV_ADDR;
                  cnt <= 3'h6;
                  start_clr_n <= 1'b0;
                  ack_ena <= 1'b0;
                  read_ena <= 1'b0;
               end
             else
               begin                      //FSM logic begins here
                  case (rI2CFsm)
                    INIT: begin
                       filter_flag <= 1'b0;
                       start_clr_n <= 1'b1;
                       ack_ena <= 1'b0;
                       read_ena <= 1'b0;
                       rDevAddr[cnt] <= sda_t[2];
                       read_flag <= 1'b0;                    //clearing read flag, in case command is a read
                       proxy_bit <= 1'b1;
                       if (start_fsm == 1'b1)
                         begin
                            cnt <= cnt - 3'h1;
                            rI2CFsm <= DEV_ADDR;            //if START happened, go to DEV_ADDR state and decrease counter
                            start_clr_n <= 1'b0;            //start condition detected, clearing the flag
                            fsm_prev_st <= rI2CFsm;
                         end
                    end
                    
                    DEV_ADDR: begin
		               start_clr_n <= 1'b1;
                       rDevAddr[cnt] <= sda_t[2];
                       if (cnt == 3'h0)
                         begin
                            cnt <= 3'h7;                            //last bit read, need to restart cnt value to 7
                            if (rDevAddr[7:1] == DEVICE_ADDR)    //if read 7-bit addr same as device addr, send ACK & continue with port address read
                              begin
                                 fsm_prev_st <= rI2CFsm;         //as we go to ACK state, we store from where we jumped, so we know where to go next
                                 rI2CFsm <= ACK_ST; 
                                 ack_ena <= 1'b1;                //enables the acknoledge to be set
                              end
                            else
                              begin
                                 rI2CFsm <= INIT;                //else, means bus access not for this device and need to clear up and
                                 rDevAddr <= 8'h00;              //go back to be ready for next cycle
                              end
                         end
                       else
                         begin
                            cnt <= cnt - 3'h1;                      //keep decrementing counter and storing ADDR bits in register
                            rI2CFsm <= DEV_ADDR;
                         end
                    end // case: DEV_ADDR
                    
                    ACK_ST:begin
                       ack_ena <= 1'b0;                        
                       case(fsm_prev_st)
                         DEV_ADDR: begin
                            if (rDevAddr[0] == 1'b0)          //write cycle
                              begin
                                 rI2CFsm <= REG_ADDR;            //then go to get register address state
                                 read_ena <= 1'b0;
                              end
                            else
                              begin
                                 rI2CFsm <= REG_READ;            //then go to get register address state
                                 read_ena <= 1'b1;
                              end
                         end
                         REG_ADDR: begin                    //if we were here, it is a write cycle
                            rI2CFsm <= REG_WRITE;           //then go to get register address state
                         end
                         REG_WRITE:begin
                            rI2CFsm <= REG_WRITE;             //if came from WRITE_REG st and no STOP nor START condition detected, keep go back to WRITE REG for more data to be read (and toggle LSb of reg-addr
                            filter_flag <= 1'b0;
                            rRegAddr[0] <= ~rRegAddr[0];
                            case (rRegAddr[2:0])
                              OUTPUT0:begin
                                 rOutput0[0] <= proxy_bit;
                              end
                              OUTPUT1:begin
                                 rOutput1[0] <= proxy_bit;
                              end
                              POLARITY0:begin
                                 rPolarity0[0] <= proxy_bit;
                              end
                              POLARITY1:begin
                                 rPolarity1[0] <= proxy_bit;
                              end
                              CONFIG0:begin
                                 rConfig0[0] <= proxy_bit;
                              end
                              CONFIG1:begin
                                 rConfig1[0] <= proxy_bit;
                              end
                              default:begin
                                 
                              end
                            endcase // case (rRegAddr[2:0])
                         end // case: WRITE_REG
                         
                            
                         default: begin
                            rI2CFsm <= INIT;
                            cnt <= 3'h7;
                            rRegAddr <= 8'h00;
                            rDevAddr <= 8'h00;
                            start_clr_n <= 1'b0;
                         end
                       endcase // case (fsm_prev_st)
                    end // case: ACK_ST
                    
                    REG_ADDR: begin                        //need to gather port address to know to which port will address
                       rRegAddr[cnt] <= sda_t[2];
                       if (cnt == 3'h0)                    //if all bits are read, go to ack state
                         begin                             
                            cnt <= 3'h7;                  
                            ack_ena <= 1'b1;
                            fsm_prev_st <= rI2CFsm;        //preserve present state in fsm_prev_st to know where to go next after ack
                            rI2CFsm <= ACK_ST;
                         end
                       else
                         begin
                            ack_ena <= 1'b0;
                            cnt <= cnt - 3'h1;
                            rI2CFsm <= REG_ADDR;
                         end
                    end // case: REG_ADDR

                    
                    REG_WRITE: begin
                       proxy_bit <= sda_t[2];
                       filter_flag <= 1'b1;
                       
                       if (cnt == 3'h0)
                         begin
                            cnt <= 3'h7;
                            rI2CFsm <= ACK_ST;
                            fsm_prev_st <= rI2CFsm;
                            ack_ena <= 1'b1;
                         end
                       else
                         begin
                            cnt <= cnt - 3'h1;
                            rI2CFsm <= REG_WRITE;
                         end // else: !if(cnt == 3'h0)

                       if (filter_flag && !stop_fsm) begin
                          case (rRegAddr)
                            OUTPUT0: begin                   
                               rOutput0[cnt + 1] <= proxy_bit;             //real write happens to the register from proxy_bit if stop bit was not asserted
                            end
                            
                            OUTPUT1: begin
                               rOutput1[cnt + 1] <=  proxy_bit;
                            end
                            
                            POLARITY0: begin
                               rPolarity0[cnt + 1] <= proxy_bit;
                            end
                         
                            POLARITY1:begin
                               rPolarity1[cnt + 1] <= proxy_bit;
                            end
                            CONFIG0:begin
                               rConfig0[cnt + 1] <= proxy_bit;
                            end
                            CONFIG1:begin
                               rConfig1[cnt + 1] <= proxy_bit;
                            end

                            default:begin
                               
                            end
                          endcase // case (rRegAddr)
                       end // if (filter_flag)
                    end // case: REG_WRITE
   
             
             
                    REG_READ: begin
                       if (cnt == 3'h0)
                         begin
                            cnt <= 3'h7;
                            rI2CFsm <= MASTER_ACK;
                            read_ena <= 1'b0;
                         end
                       else
                         begin
                            cnt <= cnt - 3'h1;
                            read_ena <= 1'b1;
                         end
                    end // case: REG_READ


                    MASTER_ACK: begin

                       if (sda_t[2]) begin
                          rI2CFsm <= INIT;
                          read_ena <= 1'b0;
                          start_clr_n <= 1'b0;
                          
                       end
                       else begin
                          rRegAddr[0] <= ~rRegAddr[0];
                          rI2CFsm <= REG_READ;
                          read_ena <= 1'b1;                     
                       end
                    end //case: MASTER_ACK

                    
                    default:begin
                       cnt <= 3'h7;
                       rI2CFsm <= INIT;
                       rRegAddr <= 8'h00;
                       rDevAddr <= 8'h00;
                       start_clr_n <= 1'b0;
                    end
                    
                  endcase // case (rI2CFsm)
               end // else: !if(start_fsm == 1'b1)
          end // else: !if(~iRst_n)
     end // always @ (posedge iSCL, negedge iRst_n)

   wire read_rst_n;
   assign read_rst_n = iRst_n /*&& !stop_fsm*/ && !wDoneTimer1ms;

   
   always @(posedge iClk, negedge read_rst_n)
     begin
        if (~read_rst_n)
          begin
             rSDAo <= 1'b1;
             rInput0_t <= 8'h00;
             rInput1_t <= 8'h00;
             ack_out <= 1'b0;
             read_out <= 1'b0;
          end
        else if (scl_fall)
          begin
             if (ack_ena == 1'b1)
               begin
                  rSDAo <= 1'b0;
                  ack_out <= 1'b1;
               end
             else if (read_ena && !stop_fsm)
               begin
                  read_out <= 1'b1;
                  ack_out <= 1'b0;

                  case(rRegAddr[2:0])
                    INPUT0: begin
                       rSDAo <= rPolarity0[cnt] ^ rInput0_sync[cnt];
                       rInput0_t[cnt] <= rInput0_sync[cnt];
                    end

                    INPUT1: begin
                       rSDAo <= rPolarity1[cnt] ^ rInput1_sync[cnt];
                       rInput1_t[cnt] <= rInput1_sync[cnt];
                    end

                    OUTPUT0: begin
                       rSDAo <= rOutput0[cnt];
                    end

                    OUTPUT1: begin
                       rSDAo <= rOutput1[cnt];
                    end
                    
                    POLARITY0: begin
                       rSDAo <= rPolarity0[cnt];
                    end

                    POLARITY1: begin
                       rSDAo <= rPolarity1[cnt];
                    end

                    CONFIG0: begin
                       rSDAo <= rConfig0[cnt];
                    end

                    CONFIG1: begin
                       rSDAo <= rConfig1[cnt];
                    end
                    
                    default:begin
                       rSDAo <= rSDAo;
                    end
                  endcase // case (rRegAddr[2:0])
               end // if (read_ena == 1'b1)
             else 
               begin
                  rSDAo <= 1'b1;
                  ack_out <= 1'b0;
                  read_out <= 1'b0;
               end // else: !if(read_ena == 1'b1)
             
          end // if (scl_fall)
     end // always @ (negedge iSCL, negedge iRst_n)
   

   assign ioSDA = (read_out || ack_out) ? (rSDAo ? 1'bZ: 1'b0) : 1'bZ;
   
   //output registers assigned to their respective outputs

   genvar idx;  //generate variable
   
   generate
      for (idx=0; idx<=7; idx=idx + 1)
        begin : output_buff
           assign ioDataP0[idx] = ~rConfig0[idx] ? rOutput0[idx] : 1'bZ;
           assign ioDataP1[idx] = ~rConfig1[idx] ? rOutput1[idx] : 1'bZ;
        end
   endgenerate

   
   assign wSmbAlert0_n = ((rInput0_t & rConfig0) == (ioDataP0 & rConfig0)) ? 1'b1 : 1'b0;
   assign wSmbAlert1_n = ((rInput1_t & rConfig1) == (ioDataP1 & rConfig1)) ? 1'b1 : 1'b0;

   assign oSmbAlert_n = (wSmbAlert0_n) ? 1'bZ : 1'b0;
   

   always @(posedge iClk)
   begin
     rInput0_meta <= rInput0;
     rInput1_meta <= rInput1;
     rInput0_sync <= rInput0_meta;
     rInput1_sync <= rInput1_meta;
   end
   
endmodule // pca9555
